import numpy as np
from scipy import stats
from Tree_ForP import DecisionTree

# 随机森林分类器类
class RandomForest:
    '''
    num_trees：
        指定了要构建的决策树的数量，默认是 5 棵树。
        比如，你可以根据数据集的规模和复杂程度等因素来调整这个数量，更多的树可能会让模型拟合能力更强，
        但也可能导致计算量增大和过拟合风险提高，默认值只是一个初始参考。

    max_depth：
        设定决策树的最大深度。
        如果设为None，决策树会不断扩展节点，直到所有叶子节点都是 “纯” 的（也就是节点内的样本都属于同一类别），
        不过太深的树容易过拟合，所以通常需要合理设置该参数来控制树的复杂度。

    max_features：
        在决策树每个节点进行分割时，只考虑这么多个特征来寻找分割阈值。
        如果是小于 1 的浮点数，代表数据集中特征的比例，比如 0.5 表示每次分割只考虑一半的特征；若为整数，就是直接指定的特征数量，限制特征数量同样是为了增加随机性和避免过拟合。

    bootstrap：
        是一个布尔值，决定是否使用带放回的自助采样方式抽取训练样本，默认是True，
        带放回采样使得某些样本可能在单棵树的训练数据中多次出现，而有些样本可能不会出现，进一步增加了随机性。

    random_state：
        用来控制整个随机森林的随机性，比如在采样和特征选择等环节都会受其影响，
        给定一个固定的整数值能保证每次运行代码得到的结果可复现，每次构建新的决策树时，
        这个值还会适当变化以保证不同树之间有不同的随机性。
    '''
    def __init__(self, num_trees=5, max_depth=None, max_features=5, bootstrap=True, random_state=55):
        self.num_trees = num_trees # 设置随机森林中，决策树的数量
        self.max_depth = max_depth # 设置决策树的最大深度，防止过拟合
        self.max_features = max_features # 随机抽取的最大特征值
        self.bootstrap = bootstrap # bool：True又放回自举，False不放回自举
        self.random_state = random_state # 方便调参
        # 用于存储 每棵训练好的决策树
        self.decision_trees = []

    # bootstrapped 自举
    # 用于fit的辅助函数
    def sample(self, X, y, random_state):
        '''
        参数: 
        X: 原数据集所有样本的特征 (行,列): (num_x,cols_feature) 
        y: 原数据集所有样本的标签 (行,): (num_x,) 
        random_state: 随机种子 用于调参

        比如: 
        X是10个样本 每个样本6个特征 shape就是(10,6)
        y是10个样本 每个样本1个标签 全部就是10个标签 shape就是(10,)

        MINST: 
        X.shape:  (60000, 784)
        y.shape:  (60000,)
        '''
 
        n_rows, n_cols = X.shape 

        # 设置随机种子，便于调参和 保证自举形成的数据集不同
        np.random.seed(random_state)
        # 返回对应样本的索引
        # 一般采用有放回的随机抽取样本，构成新的数据集，新数据集样本数和原数据集一样
        samples = np.random.choice(a=n_rows, size=n_rows, replace=self.bootstrap)
        '''
        numpy.random.choice(a, size=None, replace=True, p=None)
        从a(只要是ndarray都可以，但必须是一维的)中随机抽取数字，并组成指定大小(size)的数组
        replace:True表示可以取相同数字，False表示不可以取相同数字
        数组p：与数组a相对应，表示取数组a中每个元素的概率，默认为选取每个元素的概率相同。
        '''
        return X[samples], y[samples]
    
    # 训练随机森林
    def fit(self, X, y):
        '''
        参数: 
        X: 原数据集所有样本的特征 (行,列): (num_x,cols_feature) 
        y: 原数据集所有样本的标签 (行,): (num_x,) 
        '''
        # 重置随机森林
        if len(self.decision_trees) > 0:
            self.decision_trees = []

        # 初始化森林中决策树的数量
        num_built = 0
        # 开始训练决策树，得到随机森林
        while num_built < self.num_trees:
            # 初始化决策树
            dt = DecisionTree(
                max_depth=self.max_depth,   # 设置最大深度，防止过拟合，保证模型的泛化能力
                max_features=self.max_features, 
                random_state=self.random_state)
# TODO ---------------------------------------------------------------------------  TODO 
        # bagging只在这里和RandomForest有区别
            ''' 
            max_features:
            随机抽取的特征的最大特征数
            比如: 原数据集每个样本有10个特征, 随机抽取最多只抽取其中的4个特征, 来构建决策树
            '''

            # 获取数据样本
            _X, _y = self.sample(X, y, self.random_state)

            # 训练
            dt.fit(_X, _y)
            # 保存决策树构建随机森林
            self.decision_trees.append(dt)

            num_built += 1
            print(f'\r\aTrained Trees: {num_built}/{self.num_trees}',end='')
            # 保证每次自举的数据集不一样
            if self.random_state is not None:
                self.random_state += 1

    # 使用训练好的随机森林分类器, 预测测试集中的每个样本的类别
    def predict(self, X):
        """
        参数：     
        X: 用于测试的数据集

        返回值：
        predicted_classes: np.array 预测的类别             
        """
        # 列表中，每个元素存放了，每棵树对全部样本的预测结果
        y = []
        for tree in self.decision_trees:
            y.append(tree.predict(X))

        # 将每棵树对同一个样本的预测，放到二维数组的一行中
        y = np.swapaxes(y, axis1=0, axis2=1)

        # 取随机森林中，所有决策树中，出现次数最多的类别，作为最终随机森林预测的类别
        predicted_classes = stats.mode(y, axis=1, keepdims=True)[0].reshape(-1)
        return predicted_classes